﻿using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Xml;

using Vita.Common;
using Vita.Entities;
using Vita.Entities.Model;
using Vita.Entities.Runtime;
using Vita.Data;
using Vita.Data.Driver;
using Vita.Data.Model;

namespace Vita.Tools.DbFirst {

  using Binary = Vita.Common.Binary;


  public class DbFirstSourceWriter {
    IProcessFeedback _feedback;

    DbFirstConfig _config; 
    List<string> _warnings; 
    public bool HasErrors;
    EntityApp _app;
    System.IO.TextWriter _output;

    public DbFirstSourceWriter(IProcessFeedback feedback) {
      _feedback = feedback;
      _warnings = new List<string>(); 
    }

    public void WriteCsSources(EntityApp app, DbFirstConfig config) {
      _app = app;
      _config = config; 
      HasErrors = false; 
      var fileStream = File.Create(_config.OutputPath);
      _output = new StreamWriter(fileStream);
      WriteSource();
      _output.Flush();
      _output.Close();
    }//method


    const string FileHeader = @"
/* 
  This file was automatically generated by VITA reverse-engineering tool from the database.
  Generated date/time: {0}
*/
using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data; 

using Vita.Entities;
using Vita.Entities.Caching;
using Vita.Data;  // used only in console app
";


    private void WriteSource() {
      var header = StringHelper.SafeFormat(FileHeader, DateTime.Now.ToString("s"));
      _output.WriteLine(header);
      _output.Write("namespace {0} {{\r\n", _config.Namespace);
      foreach (var module in _app.Modules) {
        if (module.Entities.Count == 0)
          continue; 
        _output.WriteLine("// Module : " + module.Name + " --------------------------------");
        // we have 1 module per area, so all module's entities are in its area
        foreach (var entType in module.Entities) {
          var ent = _app.Model.GetEntityInfo(entType); 
          WriteEntity(ent);
        }
        _output.WriteLine(); 
      }
      var views = _app.Model.Entities.Where(e => e.Kind == EntityKind.View).ToList();
      foreach (var view in views)
        WriteEntity(view); 
      //Modules, model
      WriteModules();
      WriteEntityApp();
      if (_config.Options.IsSet(DbFirstOptions.GenerateConsoleAppCode))
        WriteConsoleAppCode(); 
      _output.WriteLine("}"); //close namespace
      if (_warnings.Count > 0) 
        _output.Write("/* Messages: \r\n" + string.Join(Environment.NewLine, _warnings) + "\r\n*/");
    }

    private void WriteEntity(EntityInfo entity) {
      _output.WriteLine();
      if (entity.Kind == EntityKind.Table)
        WriteEntityAttributes(entity); 
      // Entity header
      _output.Write("  public interface {0} {{\r\n", entity.EntityType.Name);
      //members
      foreach (var member in entity.Members) {
        if (member.Flags.IsSet(EntityMemberFlags.ForeignKey))
          continue;
        if(member.Kind == MemberKind.EntityList && !_config.Options.IsSet(DbFirstOptions.AddOneToManyLists))
          continue; 
        _output.WriteLine();
        WriteMemberAttributes(member);
        // we do not create setters for auto types and entity lists
        bool getterOnly = member.Kind == MemberKind.EntityList || member.AutoValueType != AutoType.None; 
        var accessors = getterOnly ? "{ get; }" : "{ get; set; }"; // for ent lists we have getter only
        var csName = member.DataType.GetDisplayName();
        _output.Write("    {0} {1} {2}\r\n", csName, member.MemberName, accessors);
      }
      _output.WriteLine("  }");
    }

    private void WriteEntityAttributes(EntityInfo entity) {
      // printout keys and indexes
      var attrList = new StringList();
      attrList.Add(StringHelper.SafeFormat("Entity(TableName = \"{0}\")", entity.TableName));
      foreach (var key in entity.Keys) {
        if (key.KeyType.IsSet(KeyType.ForeignKey))
          continue;
        //Skip single-column indexes - we set [Index] attribute directly on property; also skip erroneous keys with no members (if table has no PK, VITA creates PK with no columns)
        if (IsPropertyKey(key) || key.KeyMembers.Count == 0) 
          continue;
        var attrSpec = GetKeyAttribute(key, onProperty: false);
        attrList.Add(attrSpec); 
      }
      // If PK is not found in database, system creates an empty fake PK
/*
      if(entity.PrimaryKey.KeyMembers.Count == 0) {
        var warn = StringHelper.SafeFormat("  Warning: Table {0} has no primary key.", entity.TableName);
        _warnings.Add(warn); 
        _feedback.SendFeedback(FeedbackType.Warning, warn);
      }
 */ 
      _output.Write("  [{0}]\r\n", string.Join(", ", attrList));
    }

    private string GetKeyColumnList(EntityKeyInfo key) {
      var specs = new string[key.KeyMembers.Count];
      for (int i = 0; i < key.KeyMembers.Count; i++) {
        var keyMember = key.KeyMembers[i];
        var memberSpec = keyMember.Member.MemberName;// ?? keyMember.Member.MemberName;
        if (keyMember.Desc)
          memberSpec += ":DESC";
        specs[i] = memberSpec;
      }
      return string.Join(",", specs);
    }

    private void WriteMemberAttributes(EntityMemberInfo member) {
      var autoType = GetAutoType(member);
      if (autoType != null) {
        member.Flags |= EntityMemberFlags.AutoValue;
        member.AutoValueType = autoType.Value;
      }
      if (member.Flags.IsSet(EntityMemberFlags.PrimaryKey) && _config.Options.IsSet(DbFirstOptions.AutoOnGuidPrimaryKeys) && member.DataType == typeof(Guid))
        member.AutoValueType = AutoType.NewGuid;

      var dataType = member.DataType;
      var isUnlimited = member.Flags.IsSet(EntityMemberFlags.UnlimitedSize); 
      var needsSize = dataType == typeof(string) || dataType == typeof(byte[]) || dataType == typeof(Binary);
      if(isUnlimited)
        needsSize = false; 
      var isNVT = dataType.IsNullableValueType();
      var needsNullable = member.Flags.IsSet(EntityMemberFlags.Nullable) && !isNVT;
      var attrList = new StringList();
      switch (member.Kind) {
        case MemberKind.Column:
          var colArgs = new StringList();
          colArgs.Add('"' + member.ColumnName + '"');
          if (!string.IsNullOrEmpty(member.ExplicitDbTypeSpec))
            colArgs.Add("DbTypeSpec = \"" + member.ExplicitDbTypeSpec + "\"");
          else if (member.ExplicitDbType != null) 
            colArgs.Add("DbType = DbType." + member.ExplicitDbType.Value);
          if (member.DataType == typeof(Decimal)) {
            if (member.Scale != 0)
              colArgs.Add("Scale = " + member.Scale);
            if (member.Precision != 0)
              colArgs.Add("Precision = " + member.Precision); 
          }
          if (needsSize && member.Size > 0) 
            colArgs.Add("Size = " + member.Size);
          var colAttr = StringHelper.SafeFormat("Column({0})", string.Join(", ", colArgs)); 
          attrList.Add(colAttr);

          break; 
        case MemberKind.EntityRef:
          var fkMembers = member.ReferenceInfo.FromKey.ExpandedKeyMembers;
          var strCols = string.Join(",", fkMembers.Select(km => km.Member.ColumnName));
          attrList.Add(StringHelper.SafeFormat("EntityRef(KeyColumns = \"{0}\")", strCols));
          if (member.Flags.IsSet(EntityMemberFlags.CascadeDelete))
            attrList.Add("CascadeDelete");
          break; 
        case MemberKind.EntityList:
          // check if we have two or more lists of the same type - then we need to add [ManyToOne] attr to set explicitly the property name of back reference
          var count = member.Entity.Members.Where(m => m.DataType == member.DataType).Count();
          if (count > 1)
            attrList.Add(StringHelper.SafeFormat("OneToMany(\"{0}\")", member.ChildListInfo.ParentRefMember.MemberName));
          break; 
      }
      // if (needsSize)  attrList.Add(StringHelper.SafeFormat("Size({0})", member.Size));
      if (isUnlimited)
        attrList.Add("Unlimited");
      // Keys and Indexes. If there is single-column asc index on the column, put it on property directly
      var memberKeys = member.Entity.Keys.Where(key => IsPropertyKey(key) && key.KeyMembers[0].Member == member).ToList();
      foreach (var key in memberKeys) {
        var keyAttr = GetKeyAttribute(key, onProperty: true); 
        attrList.Add(keyAttr);
      }
      //Misc attributes
      if (needsNullable)
        attrList.Add("Nullable");
      if ((dataType == typeof(DateTime) || dataType == typeof(DateTime?)) && _config.Options.IsSet(DbFirstOptions.UtcDates))
        attrList.Add("Utc");

      if (member.Flags.IsSet(EntityMemberFlags.Identity))
        attrList.Add("Identity"); 

      //Auto type
      switch (member.AutoValueType) {
        case AutoType.None: break;
        case AutoType.NewGuid: attrList.Add("Auto"); break;
        case AutoType.Identity: break; //we use Identity attribute
        case AutoType.RowVersion: attrList.Add("RowVersion"); break;
        default:
          attrList.Add(StringHelper.SafeFormat("Auto(AutoType.{0})", member.AutoValueType));
          break;
      }
      // Produce final list as string
      if (attrList.Count > 0)
        _output.Write("    [{0}]\r\n", string.Join(", ", attrList));
    }

    private void WriteModules() {
      _output.WriteLine();
      _output.WriteLine("  // -------------------  Entity modules --------------------------------------");
      foreach (var module in _app.Modules) {
        if (module.Entities.Count == 0)
          continue;
        var entModuleName = module.Name;
        _output.WriteLine("  public class {0} : EntityModule {{", entModuleName);
        _output.WriteLine("    public static readonly Version CurrentVersion = new Version(\"1.0.0.0\");");
        _output.WriteLine(" ");
        _output.Write("    public {0} (EntityArea area) : base(area, \"{0}\", version: CurrentVersion) {{\r\n", entModuleName);
        _output.Write("      base.RegisterEntities(");
        //build list of types
        var entTypeList = module.Entities.Select(t => "typeof(" + t.Name + ")").ToList();
        string indent = new string(' ', 12);
        int currLen = 200; //to force start from new line
        // write in lines with len < 120
        for (var i = 0; i < entTypeList.Count; i++) {
          var entType = entTypeList[i];
          if (currLen > 120) {
            _output.WriteLine();
            _output.Write(indent);
            currLen = indent.Length;
          }
          _output.Write(entType);
          if (i < entTypeList.Count - 1)
            _output.Write(", ");
          currLen += entType.Length + 2;
        } //for i
        // close the RegisterEntities call
        _output.WriteLine(");");
        //close constructor and class
        _output.WriteLine("    }"); //method
        _output.WriteLine("  }"); //class
        _output.WriteLine();
      }//foreach area
    }

    private void WriteEntityApp() {
      _output.WriteLine("  // -------------------  Entity App --------------------------------------");
      _output.Write("  public class {0} : EntityApp {{\r\n", _config.AppClassName);
      _output.Write("    public {0} () {{\r\n", _config.AppClassName);

      //Write creation of areas and modules
      var moduleFieldDecls = new StringList();
      foreach (var module in _app.Modules) {
        if (module.Entities.Count == 0)
          continue; 
        var suffix = module.Area.Name.FirstCap();
        var areaVar = "area" + suffix;
        var moduleField = "Module" + suffix;
        _output.Write("      var {0} = this.AddArea(\"{1}\", \"{2}\");\r\n", areaVar, module.Area.Name, module.Area.Name);
        _output.Write("      {0} = new {1}({2});\r\n", moduleField, module.Name, areaVar);
        var fieldDecl = StringHelper.SafeFormat("    public readonly {0} {1};", module.Name, moduleField);
        moduleFieldDecls.Add(fieldDecl);
      }
      //close constructor and class
      _output.WriteLine("    }"); //constructor
      _output.WriteLine();
      //write Module fields
      _output.WriteLine(string.Join("\r\n", moduleFieldDecls));
      //close class
      _output.WriteLine("  }"); //class
      _output.WriteLine();
    }

    private void WriteConsoleAppCode() {
      const string template = @"
  
  // Sample Program class for a Console application. Add references to Vita assembly
  // using Vita.Entities;
  // using Vita.Entities.Caching;
  // using Vita.Data;

  class Program {
    public static {{AppClassName}} App;

    static void Main(string[] args) {
      Console.WriteLine("" Sample application for VITA-generated model. "");
      Init();
      
      //Open session and run query
      var session = App.OpenSession();
      var query = from ent in session.EntitySet<{{EntityType}}>()  // just random entity
                  // where ?condition?
                  select ent;
      var entities = query.Take(5).ToList();

      Console.WriteLine(""Loaded "" + entities.Count + "" entities."");
      foreach(var ent in entities)
        Console.WriteLine(""  Entity: "" + ent.ToString()); // change to smth more meaningful 

      Console.WriteLine(""Press any key ..."");
      Console.ReadKey();
    }
    
    private static void Init() {
      App = new {{AppClassName}}();
      // Cache settings. If you do not add any types, cacheSettings will be ignored
      App.CacheSettings.AddCachedTypes(CacheType.FullSet /* , <fully cached entity types> */ );
      App.CacheSettings.AddCachedTypes(CacheType.Sparse /* , <sparsely cached entity types> */ );
      //connect to database
      var connString = @""{{ConnectionString}}"";
      var driver = new {{DriverType}}();
      App.LogPath = ""_appLog.log"";
      var dbSettings = new DbSettings(driver, DbOptions.Default, connString, upgradeMode: DbUpgradeMode.Always);
      App.ConnectTo(dbSettings);
    }
  }
";
      var sampleEntType = _app.Modules.First(m => m.Entities.Count > 0).Entities.First();
      //string.Format() pukes on this template, so have to do it through replace
      var source = template.Replace("{{AppClassName}}", _config.AppClassName)
                           .Replace("{{ConnectionString}}", _config.ConnectionString)
                           .Replace("{{EntityType}}", sampleEntType.Name)
                           .Replace("{{DriverType}}", _config.Driver.GetType().FullName);
      _output.WriteLine(source); 
    }

    private AutoType? GetAutoType(EntityMemberInfo member) {
      AutoType result;
      if (_config.AutoValues.TryGetValue(member.MemberName, out result)) return result;
      if (_config.AutoValues.TryGetValue(member.ColumnName, out result)) return result;
      return null; 
    }

    private bool IsPropertyKey(EntityKeyInfo key) {
      if (key.KeyMembers.Count != 1)  return false;
      if(key.KeyMembers[0].Desc) return false; 
      // Only Index or PK - but not ForeignKey; FK is hidden key, without attribute
      bool isIndexOrPk = key.KeyType.IsSet(KeyType.Index | KeyType.PrimaryKey);
      if (!isIndexOrPk)
        return false; 
      //if it is a key on fk column that is NOT explicitly exposed as property
      if (key.KeyMembers.Any(km => km.Member.Flags.IsSet(EntityMemberFlags.ForeignKey)))
        return false; 
      return true; 
    }

    private bool CanBeIdentity(Type type) {
      return type == typeof(int) || type == typeof(uint) || type == typeof(long) || type == typeof(ulong);
    }

    private string GetKeyAttribute(EntityKeyInfo key, bool onProperty) {
      var attrName = GetKeyAttributeName(key.KeyType);
      var args = new List<string>(); 
      if(!onProperty)
        args.Add(DQuote(GetKeyColumnList(key)));
      if(!string.IsNullOrWhiteSpace(key.Filter))
        args.Add("Filter = " + DQuote(key.Filter));
      if(key.IncludeMembers.Count > 0) {
        var sInc = string.Join(",", key.IncludeMembers.Select(m => m.MemberName));
        args.Add("IncludeMembers = " + DQuote(sInc));
      }
      if(key.KeyType.IsSet(KeyType.PrimaryKey) && key.KeyType.IsSet(KeyType.Clustered))
        args.Add("IsClustered=true");
      if(args.Count == 0)
        return attrName;
      var result = attrName + "(" + string.Join(", ", args) + ")";
      return result;
    }

    private static string DQuote(string value) {
      return "\"" + value + "\"";
    }

    private string GetKeyAttributeName(KeyType keyType) {
      if (keyType.IsSet(KeyType.PrimaryKey))
        return "PrimaryKey";
      if (keyType.IsSet(KeyType.Clustered)) 
        return keyType.IsSet(KeyType.Unique) ? "UniqueClusteredIndex" : "ClusteredIndex";
      if (keyType.IsSet(KeyType.Unique))
        return "Unique";
      if (keyType.IsSet(KeyType.Index))
        return "Index";
      Util.Throw("Invalid key type for attribute: " + keyType);
      return null; 
    }


  } //class

}//ns
